/**
 * @file w5100.c
 * @brief Implements functions declared in the W5100 module. Certain parts
 *        of this implementation are specific to how the CC2530 has been 
 *        interfaced with the CC2530 in the Wizi project. The CC2530 specific
 *        implementation has been limited to certain include files 
 *        (ex: w5100_wizi.h).
 * @author Vamshi Ponnapalli
 */

/*******************************************************************************
 * Includes
 ******************************************************************************/
#include "common.h"
#include "hal_types.h"
#include "w5100.h"
//#include "system.h"
#include "w5100_wizi.h"

/*******************************************************************************
 * Variable declarations
 ******************************************************************************/
//Static variables (private to this file)
/**
 * Structures that contain socket information.
 */
static socket_info_t socketInfo[4];

/*******************************************************************************
 * Static function declarations (private to this file)
 ******************************************************************************/
/**
 * Writes a series of bytes to the ethernet device.
 *
 * @param address A 16 bit address that identifies the location on the device to
 *                which the the bytes will be written. For multi byte writes,
 *                the location is the starting point of the write operation.
 * @param pData A reference to a buffer of bytes that will be written to the 
 *              address on the device.
 * @param size The size of the data buffer.
 */
static void __w5100_writeData(uint16 address, const uint8 *pData, uint16 size);

/**
 * Writes a word of data to the ethernet device. This call should be used when
 * the two bytes of the word make up a number (as against just a series 
 * of bytes).
 * 
 * This specifically applies to situations where the processor uses little
 * endian formats and the ethernet device uses a big endian format.
 *
 * @param address A 16 bit address that identifies the location on the device to 
 *                which the the bytes will be written to. For multi byte writes,
 *                the location is the starting point of the write operation.
 * @param word The word of data to be written to the specified address.
 */
static void __w5100_writeWord(uint16 address, uint16 word);

/**
 * Reads a series of bytes from the ethernet device.
 *
 * @param address A 16 bit address that identifies the location on the device to 
 *                which the the bytes will be read from. For multi byte writes,
 *                the location is the starting point of the read operation.
 * @param pData A reference to a buffer of bytes that will contain the data after
 *              the read operation is completed.
 * @param size The size of the data read from the ethernet device.
 */
static void __w5100_readData(uint16 address, uint8 *pData, uint16 size);

/**
 * Reads a word of data from the ethernet device. This call should be used when
 * the sequence of data reads matters to the data extracted, or when the two 
 * bytes of the word make up a number (as against just a series of bytes).
 * 
 * For example, when reading the available free Tx/Rx memory, the MSB must be 
 * read first before the LSB is read in order to ensure that the correct value 
 * is retrieved. This call should be used under such circumstances.
 *
 * @param address A 16 bit address that identifies the location on the device to 
 *                which the the bytes will be read from. For multi byte writes,
 *                the location is the starting point of the read operation.
 * 
 * @return The word of data read from the specified address.
 */
static uint16 __w5100_readWord(uint16 address);
/*******************************************************************************
 * Function implementations
 ******************************************************************************/
void w5100_reset(void)
{
  //Reset the ethernet port.
  W5100_RESET_ON;
  /**
   * According to the datasheet, the W5100 is supposed to be in reset for at
   * for at least 2 us. Let us give it 10.
   */
  utils_sleepUs(10);

  //Undo the reset signal.
  W5100_RESET_OFF;

  //Give it a bit of time before using it for anything
  utils_sleepUs(100);
}
/******************************************************************************/
void w5100_setAddressingMode(uint8 mode)
{
  W5100_DATA_PORT = mode; //Set the addressing mode.
  W5100_ADDR0_PIN = 0;
  W5100_ADDR1_PIN = 0;
  W5100_TRIGGER_WRITE; //Trigger the write operation.
}
/******************************************************************************/
void w5100_setInterruptMask(uint8 mask)
{
  //Set the interrupts.
  __w5100_writeData(W5100_REG_INTERRUPT_MASK, &mask, 1);
}
/******************************************************************************/
void w5100_initializeTxMemory(uint8 s0Size, uint8 s1Size, 
                                                    uint8 s2Size, uint8 s3Size)
{
  uint8 bufferSizes;
  bufferSizes = ((s3Size << 6) | (s1Size << 2) | (s2Size << 4) | s0Size);
  
  //Set Tx buffer size
  __w5100_writeData(W5100_REG_TX_BUF_SIZE, &bufferSizes, 1);

  //Compute mask and base address values for the Tx buffers
  socketInfo[0].txAddressMask = (W5100_MASK_BASE << s0Size) - 1;
  socketInfo[1].txAddressMask = (W5100_MASK_BASE << s1Size) - 1;
  socketInfo[2].txAddressMask = (W5100_MASK_BASE << s2Size) - 1;
  socketInfo[3].txAddressMask = (W5100_MASK_BASE << s3Size) - 1;
  
  socketInfo[0].txAddressBase = W5100_TX_ADDRESS_BASE;
  socketInfo[1].txAddressBase = socketInfo[0].txAddressBase +
                                              socketInfo[0].txAddressMask + 1;
  socketInfo[2].txAddressBase = socketInfo[1].txAddressBase +
                                              socketInfo[1].txAddressMask + 1;
  socketInfo[3].txAddressBase = socketInfo[2].txAddressBase + 
                                              socketInfo[2].txAddressMask + 1;  
}
/******************************************************************************/
void w5100_initializeRxMemory(uint8 s0Size, uint8 s1Size, 
                                                    uint8 s2Size, uint8 s3Size)
{
  uint8 bufferSizes;
  bufferSizes = ((s3Size << 6) | (s1Size << 2) | (s2Size << 4) | s0Size);
  
  //Set Rx buffer size
  __w5100_writeData(W5100_REG_RX_BUF_SIZE, &bufferSizes, 1);

  //Compute mask and base address values for the Rx buffers
  socketInfo[0].rxAddressMask = (W5100_MASK_BASE << s0Size) - 1;
  socketInfo[1].rxAddressMask = (W5100_MASK_BASE << s1Size) - 1;
  socketInfo[2].rxAddressMask = (W5100_MASK_BASE << s2Size) - 1;
  socketInfo[3].rxAddressMask = (W5100_MASK_BASE << s3Size) - 1;
  
  socketInfo[0].rxAddressBase = W5100_RX_ADDRESS_BASE;
  socketInfo[1].rxAddressBase = socketInfo[0].rxAddressBase + 
                                              socketInfo[0].rxAddressMask + 1;
  socketInfo[2].rxAddressBase = socketInfo[1].rxAddressBase +
                                              socketInfo[1].rxAddressMask + 1;
  socketInfo[3].rxAddressBase = socketInfo[2].rxAddressBase +
                                              socketInfo[2].rxAddressMask + 1;  
}
/******************************************************************************/
void w5100_setMacAddress(mac_address_t macAddress)
{
  __w5100_writeData(W5100_REG_HARDWARE_ADDR, (uint8*)&macAddress, 6);
}
/******************************************************************************/
void w5100_setLocalIPAddress(ip_address_t ipAddress)
{
  __w5100_writeData(W5100_REG_IP_ADDR, (uint8*)&ipAddress, 4);
}
/******************************************************************************/
void w5100_setRemoteIPAddress(uint8 socket, ip_address_t ipAddress)
{
  __w5100_writeData(W5100_REG_DEST_IP_ADDR(socket), (uint8*)&ipAddress, 4);
}
/******************************************************************************/
void w5100_setLocalPortNumber(uint8 socket, uint16 portNumber)
{
  __w5100_writeWord(W5100_REG_LOCAL_PORT(socket), portNumber);
}
/******************************************************************************/
void w5100_setRemotePortNumber(uint8 socket, uint16 portNumber)
{
  __w5100_writeWord(W5100_REG_DEST_PORT(socket), portNumber);
}
/******************************************************************************/
uint8 w5100_getSocketState(uint8 socket)
{
  uint8 state = 0x00;
  __w5100_readData(W5100_REG_SOCK_STAT(socket), (uint8*)&state, 1);
  
  return(state);
}
/******************************************************************************/
void w5100_openSocket(uint8 socket, uint8 mode)
{ 
  //Set mode.
  uint8 byte;
  
  //Determine the current mode register value.
  __w5100_readData(W5100_REG_SOCK_MODE(socket), &byte, 1);  
  
  //Set the socket mode
  byte = (byte & 0xF0) | mode; 
  __w5100_writeData(W5100_REG_SOCK_MODE(socket), &byte, 1);
  
  //Issue the open command.
  byte = W5100_SOCK_CMD_OPEN; 
  __w5100_writeData(W5100_REG_SOCK_CMD(socket), &byte, 1);  

  //Give it some time before returning.
  utils_sleepUs(10);
}
/******************************************************************************/
uint8 w5100_connect(uint8 socket, uint8 attempts)
{
  do
  { 
    //Read the state of the socket.
    uint8 byte = w5100_getSocketState(socket);
    if(byte == 0x13)
    {            
      byte = W5100_SOCK_CMD_CONNECT;
      __w5100_writeData(W5100_REG_SOCK_CMD(socket), &byte, 1);  
      return(SUCCESS);
    }
      
    //Give it some time before returning.
    utils_sleepUs(10);
  }
  while(--attempts > 0);
  
  return(FAILED);
}
/******************************************************************************/
void w5100_closeSocket(uint8 socket)
{
  //Close the host channel
  uint8 cmd = W5100_SOCK_CMD_CLOSE;
      __w5100_writeData(W5100_REG_SOCK_CMD(socket), &cmd, 1);
      
  /**
   * Reset interrupt flags when the socket is closed. Why? Not sure just yet,
   * but this does fix some unpredictable behavior seen during debugging.
   * 
   * The debugging behavior involved interrupts being triggered on alternate 
   * debugging cycles, as soon as interrupts were enabled (no interrupts are
   * even expected at this point).
   */
  //TODO: Review this
  cmd = 0xFF;
  __w5100_writeData(W5100_REG_SOCK_INT(socket), &cmd, 1);
}
/******************************************************************************/
uint8 w5100_Transmit(uint8 socket, uint8 *pData, uint16 size)
{
  //Check available transmit buffer space.
  if(size > __w5100_readWord(W5100_REG_TX_FREE_MEM(socket)))
  {
    //Transmit buffer has no space left.
    return(FAILED);
  }
  
  uint16 writePointer;
  uint16 writeAddress;
  uint16 offset;
  
  //Read the write pointer from the device.
  writePointer = __w5100_readWord(W5100_REG_TX_WRITE_PTR(socket));
  
  //Now determine physical write address.
  socket_info_t *sock = &socketInfo[socket];
  offset = (sock->txAddressMask & writePointer);
  writeAddress = sock->txAddressBase + offset;

  //Check if the available space wraps around the buffer.
  if((offset + size) > sock->txAddressMask + 1)
  {
    //Write the first section of data (before it wraps)
    uint16 sectionSize = sock->txAddressMask + 1 - offset;
    __w5100_writeData(writeAddress, pData, sectionSize);
    
    //Now write the second section of the data (after it wraps)
    pData += sectionSize;
    sectionSize = size - sectionSize;
    __w5100_writeData(sock->txAddressBase, pData, sectionSize);
  }
  else
  {
    //There is no wrapping. Just write the data.
    __w5100_writeData(writeAddress, pData, size);
  }
  
  //Update the write pointer based on the number of bytes written
  writePointer += size;
  __w5100_writeWord(W5100_REG_TX_WRITE_PTR(socket), writePointer);
  
  //Request the ethernet device to send the data.
  uint8 cmd = W5100_SOCK_CMD_SEND;
 __w5100_writeData(W5100_REG_SOCK_CMD(socket), &cmd, 1);
 
  return(SUCCESS);
}
/******************************************************************************/
uint8 w5100_Receive(uint8 socket, uint8 *pData, uint16 size)
{
  //Check available data to be read.
  if(size > __w5100_readWord(W5100_REG_RX_DATA_SIZE(socket)))
  {
    //There's not enough data to be read. Return an error..
    return(FAILED);
  }

  uint16 readPointer;
  uint16 readAddress;
  uint16 offset;
  
  //Read the read pointer from the device.
  readPointer = __w5100_readWord(W5100_REG_RX_READ_PTR(socket));
  
  //Now determine physical write address.
  socket_info_t *sock = &socketInfo[socket];
  offset = (sock->rxAddressMask & readPointer);
  readAddress = sock->rxAddressBase + offset;

  //Only read the data if the buffer is not NULL.
  if (pData != NULL) 
  {
    //Check if the available data wraps around the buffer.
    if((offset + size) > sock->rxAddressMask + 1)
    {
      //Read the first section of data (before it wraps)
      uint16 sectionSize = sock->rxAddressMask + 1 - offset;
      __w5100_readData(readAddress, pData, sectionSize);
      
      //Now read the second section of the data (after it wraps)
      pData += sectionSize;
      sectionSize = size - sectionSize;
      __w5100_readData(sock->rxAddressBase, pData, sectionSize);
    }
    else
    {
      __w5100_readData(readAddress, pData, size);
    }
  }
  
  //Update the read pointer based on the number of bytes read
  readPointer += size;
  __w5100_writeWord(W5100_REG_RX_READ_PTR(socket), readPointer);
   
   //Request the ethernet device to send the data.
   uint8 cmd = W5100_SOCK_CMD_RECV;
  __w5100_writeData(W5100_REG_SOCK_CMD(socket), &cmd, 1);

  return(SUCCESS);
}
/******************************************************************************/
uint8 w5100_getEthernetEventState(void)
{  
  uint8 state;
  uint8 cmd;
  
  __w5100_readData(W5100_REG_INTERRUPT, &state, 1);
  if(state != 0x00)
  {
    //Reset the interrupt flags.
    cmd = state & 0xF0;
    __w5100_writeData(W5100_REG_INTERRUPT, &cmd, 1);
  }
  return(state);
}
/******************************************************************************/
uint8 w5100_getSocketEventState(uint8 socket)
{
  uint8 state;
  uint8 cmd;
  
  __w5100_readData(W5100_REG_SOCK_INT(socket), &state, 1);
  if(state != 0x00)
  {
    //Reset the interrupt flags.  
    cmd = state;
    __w5100_writeData(W5100_REG_SOCK_INT(socket), &cmd, 1);
  }
  return(state);
}
/******************************************************************************/
uint16 w5100_getAvailableDataSize(uint8 socket)
{
  //Read and return the data size.
  return(__w5100_readWord(W5100_REG_RX_DATA_SIZE(socket)));
}
/******************************************************************************/
void w5100_setSubnetMask(ip_address_t mask)
{
  __w5100_writeData(W5100_REG_SUBNET_MASK, (uint8*)&mask, 4);
}
/******************************************************************************/
void w5100_setGatewayAddress(ip_address_t gatewayAddress)
{
  __w5100_writeData(W5100_REG_GATEWAY_ADDR, (uint8*)&gatewayAddress, 4);
}
/******************************************************************************/
static uint16 __w5100_readWord(uint16 address)
{
  uint8 byte = 0x00;
  uint16 word = 0x0000;
  
  //Read MSB
  __w5100_readData(address, (uint8*)&byte, 1);
  word = byte << 8;
  
  //Read LSB
  __w5100_readData(address+1, (uint8*)&byte, 1);
  word |= byte;
 
  return(word);
}
/******************************************************************************/
static void __w5100_writeWord(uint16 address, uint16 word)
{  
  uint8 byte = 0x00;
  
  //Write MSB
  byte = word >> 8;
  __w5100_writeData(address, (uint8*)&byte, 1);
  
  //Write LSB
  byte = word;
  __w5100_writeData(address+1, (uint8*)&byte, 1);
}
/******************************************************************************/
static void __w5100_writeData(uint16 address, const uint8 *pData, uint16 size)
{    
  /**
   * Set the data mode to "write", i.e., all data sent to the ethernet data port
   * will be written to the port.
   */
  W5100_DATA_MODE = W5100_DATA_MODE_WRITE;
  
  //Write the address MSB
  W5100_DATA_PORT = address >> 8; 
  W5100_ADDR0_PIN = 1;
  W5100_ADDR1_PIN = 0;  
  W5100_TRIGGER_WRITE; //Trigger the write operation.
  
  
  //Write the address LSB
  W5100_DATA_PORT = address; 
  W5100_ADDR0_PIN = 0;
  W5100_ADDR1_PIN = 1;  
  W5100_TRIGGER_WRITE; //Trigger the write operation.
  
  /**
   * Write the contents of the buffer - auto-address-increment is on, so the 
   * address need not be reset for each byte.
   */
  W5100_ADDR0_PIN = 1;    
  W5100_ADDR1_PIN = 1;

        
  while (size > 0) 
  {    
    //Set the data
    W5100_DATA_PORT = *pData;    
    W5100_TRIGGER_WRITE; //Trigger the write operation.

    //Move to the next byte of data
    pData++;
    
    size--;
  }
  
}

/******************************************************************************/
static void __w5100_readData(uint16 address, uint8 *pData, uint16 size)
{
  /**
   * Set the data mode to "write", i.e., all data sent to the ethernet data port
   * will be written to the port. We need this because the address has to first
   * be written to the device before a read operation can be performed.
   */
  W5100_DATA_MODE = W5100_DATA_MODE_WRITE;
  
  //Write the address MSB
  W5100_DATA_PORT = address >> 8; 
  W5100_ADDR0_PIN = 1;
  W5100_ADDR1_PIN = 0;  
  W5100_TRIGGER_WRITE; //Trigger the write operation.
  
  
  //Write the address LSB
  W5100_DATA_PORT = address; 
  W5100_ADDR0_PIN = 0;
  W5100_ADDR1_PIN = 1;  
  W5100_TRIGGER_WRITE; //Trigger the write operation.

  /**
   * Set the data mode to "read", i.e., all data sent to the ethernet data port
   * will be read from the port.
   */
  W5100_DATA_MODE = W5100_DATA_MODE_READ;
  
  /**
   * Write the contents of the buffer - auto-address-increment is on, so the 
   * address need not be reset for each byte.
   */
  W5100_ADDR0_PIN = 1;    
  W5100_ADDR1_PIN = 1;
  
  while (size > 0) 
  {
    W5100_TRIGGER_READ(pData); //Trigger the read operation.
    
    //Move to the next byte of data
    pData++;
    size--;
  }
}
/******************************************************************************/